﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using log4net;
using RakutenCrawler.ConfigClass;
using RakutenCrawler.Repository;
using RakutenCrawler.PageProcessors;

namespace RakutenCrawler
{
    public class CrawlerEngine
    {
        private ILog _log = LogManager.GetLogger("Rakuten");

        private List<LinkConfig> GetAllLinkConfigs()
        {
            var xmlSerialzier = new XmlSerializer<CrawlerConfig>();
            var config = xmlSerialzier.Deserialize("Config/crawler-config.xml");

            return config.Links.Cast<LinkConfig>().ToList();
        }


        private void CheckForNewScheduler()
        {
            var schedulerRepo = new SchedulerRepository();
            var processTrackingRepo = new ProcessTrackingRepository();

            _log.Info("Start checking for new scheduler");

            try
            {
                var schedulers = schedulerRepo.All.ToList().Where(s => !processTrackingRepo.All.Any(p => p.LinkConfigGuid == s.LinkConfigGuid));

                foreach (var scheduler in schedulers)
                {
                    _log.Info("Insert new scheduler - GUID: " + scheduler.LinkConfigGuid);

                    var newProcessTrack = new ProcessTracking { LinkConfigGuid = scheduler.LinkConfigGuid, CrawlStatus = Constants.NotWorkingStatus };
                    processTrackingRepo.InsertOrUpdate(newProcessTrack);
                }

                processTrackingRepo.Save();
            }
            catch (Exception e)
            {
                _log.Error("CheckForNewScheduler Exception", e);
            }
            finally
            {
                schedulerRepo.Dispose();
                processTrackingRepo.Dispose();
            }
        }

        private List<Link> GetAllLinksMatchLinkConfigPattern(LinkConfig linkConfig)
        {
            using (var linkRepo = new LinkRepository())
            {
                var dt = DateTime.Now;
                var allLinks = linkRepo.FindBy(l => !l.IsDelete).ToList();
                return allLinks.Where(l => linkConfig.IsSatisfy(l.Url)).ToList();
            }
        }

        public void ExecuteNewScheduler()
        {
            var processRepo = new ProcessTrackingRepository();

            var isParallel = MediaBot.Library.ConfigurationSettings.Settings.IsRunParallelOnSite;
            var parallism = MediaBot.Library.ConfigurationSettings.Settings.NumberOfThreadsInPool;

            try
            {
                CheckForNewScheduler();

                var linkConfigs = GetAllLinkConfigs();

                var firstRunProcesses = processRepo.FindBy(p => p.LastRunningDate == null && p.IsDelete == false).ToList();

                if (isParallel)
                {
                    Parallel.ForEach(firstRunProcesses, p =>
                    {
                        ExecuteProcess(p, linkConfigs, isParallel, parallism);
                    });
                }
                else
                    foreach (var process in firstRunProcesses)
                    {
                        ExecuteProcess(process, linkConfigs, isParallel, parallism);
                    }
            }
            catch (Exception e)
            {
                _log.Error("CrawlerEngine.ExecuteNewScheduler() exception", e);
            }
            finally
            {
                processRepo.Dispose();
            }
        }

        public void ExecuteScheduledProcess()
        {
            var processRepo = new ProcessTrackingRepository();
            var isParallel = MediaBot.Library.ConfigurationSettings.Settings.IsRunParallelOnSite;
            var parallism = MediaBot.Library.ConfigurationSettings.Settings.NumberOfThreadsInPool;

            try
            {
                var linkConfigs = GetAllLinkConfigs();

                var dt = DateTime.Now;
                var resumeProcess = processRepo.FindBy(p => p.CrawlStatus == Constants.ProcessingStatus && !p.IsDelete);
                var onThisDayScheduledProcesses = processRepo.FindBy(p => p.NextRunningDate.HasValue && p.NextRunningDate.Value.Day == dt.Day && p.NextRunningDate.Value.Month == dt.Month && p.NextRunningDate.Value.Year == dt.Year
                    && p.CrawlStatus == Constants.CompletedStatus && !p.IsDelete);

                var processes = new List<ProcessTracking>(resumeProcess);
                processes.AddRange(onThisDayScheduledProcesses);

                if (isParallel)
                {
                    Parallel.ForEach(processes, p =>
                    {
                        ExecuteProcess(p, linkConfigs, isParallel, parallism);
                    });
                }
                else
                    foreach (var process in processes)
                    {
                        ExecuteProcess(process, linkConfigs, isParallel, parallism);
                    }
            }
            catch (Exception e)
            {
                _log.Error("CrawlerEngine.ExecuteScheduledProcess() exception", e);
            }
            finally
            {
                processRepo.Dispose();
            }

        }


        private void ExecuteProcess(ProcessTracking process, List<LinkConfig> linkConfigs, bool isParallel, int numberOfThread)
        {
            var processTrackingRepo = new ProcessTrackingRepository();
            var linkRepo = new LinkRepository();
            int successCount = 0, errorCount = 0;

            try
            {
                process.CrawlStatus = Constants.ProcessingStatus;
                process.LastRunningDate = DateTime.Now;
                processTrackingRepo.InsertOrUpdate(process);
                processTrackingRepo.Save();

                var linkConfig = linkConfigs.FirstOrDefault(l => l.Guid == Guid.Parse(process.LinkConfigGuid));
                if (linkConfig == null)
                    throw new NotImplementedException("There is no config for GUID: " + process.LinkConfigGuid);

                var links = GetAllLinksMatchLinkConfigPattern(linkConfig);

                #region Run parallel

                if (isParallel)
                {
                    var option = new ParallelOptions { MaxDegreeOfParallelism = numberOfThread };
                    var syncObject = new Object();


                    Parallel.ForEach(links, option, (link) =>
                        {
                            var isComplete = true;
                            if (ShouldProcessThisLink(link))
                            {
                                lock (syncObject)
                                {
                                    BeginCrawlLink(link, linkRepo);
                                }
                                

                                var processor = PageProcessorFactory.CreatePageProcessor(linkConfig, link.LinkId);
                                isComplete = processor.Execute();
                                UpdateLinkStatus(link, isComplete);
                            }

                            lock (syncObject)
                            {
                                if (isComplete) successCount += 1; else errorCount += 1;
                                var total = successCount + errorCount;
                                if (total >= links.Count)
                                    OnProcessExecuteCompleted(process.ProcessTrackingId, successCount, errorCount);
                            };
                        });

                }
                else
                {
                    foreach (var link in links)
                    {
                        if (!ShouldProcessThisLink(link))
                        {
                            successCount += 1;
                            continue;
                        }


                        BeginCrawlLink(link, linkRepo);

                        var processor = PageProcessorFactory.CreatePageProcessor(linkConfig, link.LinkId);
                        var isComplete = processor.Execute();
                        UpdateLinkStatus(link, isComplete);

                        if (isComplete)
                            successCount += 1;
                        else
                            errorCount += 1;
                    }

                    OnProcessExecuteCompleted(process.ProcessTrackingId, successCount, errorCount);
                }

                #endregion
            }
            catch (Exception e)
            {
                _log.Error("ExecuteProcess: " + process.ProcessTrackingId + " exception", e);
            }
            finally
            {
                processTrackingRepo.Dispose();
                linkRepo.Dispose();
            }
        }

        private void OnProcessExecuteCompleted(int processId, int successCount, int errorCount)
        {
            var scheduleRepo = new SchedulerRepository();
            var historyRepo = new CrawlHistoryRepository();
            var trackingRepo = new ProcessTrackingRepository();
            try
            {
                var processTracking = trackingRepo.Single(p => p.ProcessTrackingId == processId);
                processTracking.CrawlStatus = Constants.CompletedStatus;

                var scheduler = scheduleRepo.Single(s => s.LinkConfigGuid == processTracking.LinkConfigGuid);
                if (scheduler.Days > 0)
                {
                    var nextRunningDate = processTracking.LastRunningDate.Value.AddDays(scheduler.Days);
                    processTracking.NextRunningDate = nextRunningDate;
                }

                trackingRepo.InsertOrUpdate(processTracking);
                trackingRepo.Save();


                var history = new CrawlHistory { CrawlDate = processTracking.LastRunningDate.Value, SuccessCount = successCount, ErrorCount = errorCount, ProcessTrackingId = processId };

                historyRepo.InsertOrUpdate(history);
                historyRepo.Save();
            }
            catch (Exception e)
            {
                _log.Error("On Process Update Completed Process Exception. ProcessId: " + processId, e);
            }
            finally
            {
                trackingRepo.Dispose();
                historyRepo.Dispose();
                scheduleRepo.Dispose();
            }
        }


        private static void UpdateLinkStatus(Link link, bool isCompleted)
        {
            var linkRepo = new LinkRepository();
            link.CrawlStatus = isCompleted ? Constants.CompletedStatus : Constants.CompletedStatus;
            linkRepo.InsertOrUpdate(link);
            linkRepo.Save();

            linkRepo.Dispose();
        }

        private static void BeginCrawlLink(Link link, LinkRepository linkRepo)
        {
            //var linkRepo = new LinkRepository();
            try
            {
                link.CrawlStatus = Constants.ProcessingStatus;
                link.CrawlDate = DateTime.Now;
                linkRepo.InsertOrUpdate(link);
                linkRepo.Save();
            }
            finally
            {
                //linkRepo.Dispose();
            }                   
        }

        private bool ShouldProcessThisLink(Link link)
        {
            if (link.CrawlDate.HasValue && link.CrawlDate.Value.Date == DateTime.Now.Date && link.CrawlStatus == Constants.CompletedStatus)
                return false;

            return true;
        }
    }
}
